import numpy as np
from utils.db import get_mysql_conn, get_redis_conn
from models.collaborative_filtering.item_cf import ItemCF
from models.content_based.content_based_improved import ContentBasedRecommender
from config.config import RECOMMENDATION_CONFIG
import json

class HybridRecommender:
    """混合推荐算法，结合协同过滤和基于内容的推荐"""
    
    def __init__(self):
        # 初始化推荐器
        self.item_cf = ItemCF()
        self.content_recommender = ContentBasedRecommender()
        
        # 从配置中获取权重
        self.cf_weight = RECOMMENDATION_CONFIG.get('cf_weight', 0.7)
        self.content_weight = RECOMMENDATION_CONFIG.get('content_weight', 0.3)
        
        # 数据库连接
        self.mysql_conn = get_mysql_conn()
        self.redis_conn = get_redis_conn()
        
        # 初始化模型
        self._init_models()
    
    def _init_models(self):
        """初始化各个推荐模型"""
        # 尝试从Redis加载协同过滤相似度数据
        cf_loaded = self.item_cf.load_similarity_from_redis()
        if not cf_loaded:
            print("Redis中没有协同过滤相似度数据，需要先训练模型")
        
        # 尝试从Redis加载内容相似度数据
        content_loaded = self.content_recommender.load_similarity_from_redis()
        if not content_loaded:
            print("Redis中没有内容相似度数据，需要先训练模型")
    
    def train_models(self):
        """训练所有推荐模型"""
        print("开始训练协同过滤模型...")
        cf_success = self.item_cf.train_model()
        
        print("开始训练基于内容的推荐模型...")
        content_success = self.content_recommender.train_model()
        
        return cf_success and content_success
    
    def get_user_selected_courses(self, user_id):
        """获取用户已选课程"""
        try:
            if not self.mysql_conn:
                self.mysql_conn = get_mysql_conn()
                
            # 查询用户的购买和完成过的课程
            query = """
            SELECT DISTINCT course_id
            FROM user_behaviors
            WHERE user_id = %s AND event_type IN ('purchase', 'complete')
            """
            
            with self.mysql_conn.cursor() as cursor:
                cursor.execute(query, (user_id,))
                results = cursor.fetchall()
                
            selected_courses = [row['course_id'] for row in results]
            return selected_courses
            
        except Exception as e:
            print(f"获取用户已选课程失败: {e}")
            return []
    
    def recommend(self, user_id, n=10):
        """
        为用户生成混合推荐
        
        参数:
            user_id: 用户ID
            n: 推荐课程数量
            
        返回:
            推荐课程列表，格式为 [(course_id, score), ...]
        """
        # 获取用户已选课程
        selected_courses = self.get_user_selected_courses(user_id)
        
        # 获取协同过滤推荐结果
        cf_recommendations = self.item_cf.recommend(user_id, n=n*2, exclude_courses=selected_courses)
        
        # 获取基于内容的推荐结果
        content_recommendations = self.content_recommender.recommend_by_content(user_id, n=n*2, exclude_courses=selected_courses)
        
        # 如果任一推荐方式失败，则使用另一种方式的结果
        if not cf_recommendations:
            print(f"用户 {user_id} 没有协同过滤推荐结果，使用基于内容的推荐")
            return content_recommendations[:n]
            
        if not content_recommendations:
            print(f"用户 {user_id} 没有基于内容的推荐结果，使用协同过滤推荐")
            return cf_recommendations[:n]
        
        # 合并两种推荐结果
        # 1. 先将两种推荐结果转换为字典
        cf_dict = {course_id: score for course_id, score in cf_recommendations}
        content_dict = {course_id: score for course_id, score in content_recommendations}
        
        # 2. 归一化评分
        def normalize_scores(scores):
            max_score = max(scores.values()) if scores else 1
            min_score = min(scores.values()) if scores else 0
            range_score = max_score - min_score if max_score > min_score else 1
            
            return {k: (v - min_score) / range_score for k, v in scores.items()}
        
        normalized_cf = normalize_scores(cf_dict)
        normalized_content = normalize_scores(content_dict)
        
        # 3. 计算混合评分
        hybrid_scores = {}
        all_course_ids = set(normalized_cf.keys()) | set(normalized_content.keys())
        
        for course_id in all_course_ids:
            cf_score = normalized_cf.get(course_id, 0)
            content_score = normalized_content.get(course_id, 0)
            
            # 使用加权混合评分
            hybrid_score = cf_score * self.cf_weight + content_score * self.content_weight
            hybrid_scores[course_id] = hybrid_score
        
        # 4. 排序并返回前N个推荐
        recommendations = sorted(hybrid_scores.items(), key=lambda x: x[1], reverse=True)[:n]
        
        # 5. 获取推荐课程的详细信息
        if recommendations:
            self._enrich_recommendations(recommendations)
        
        return recommendations
    
    def _enrich_recommendations(self, recommendations):
        """给推荐结果添加详细信息"""
        try:
            if not self.mysql_conn:
                self.mysql_conn = get_mysql_conn()
            
            # 获取所有推荐课程ID
            course_ids = [str(course_id) for course_id, _ in recommendations]
            ids_str = ",".join(course_ids)
            
            # 查询课程详细信息
            query = f"""
            SELECT id, title, category, tags, difficulty, teacher_name,
                   (SELECT AVG(CAST(JSON_EXTRACT(extra_data, '$.rating') AS DECIMAL(3,1)))
                    FROM user_behaviors
                    WHERE event_type = 'rate' AND course_id = courses.id) AS avg_rating
            FROM courses
            WHERE id IN ({ids_str})
            """
            
            with self.mysql_conn.cursor() as cursor:
                cursor.execute(query)
                results = cursor.fetchall()
            
            # 创建课程信息字典
            course_info = {row['id']: row for row in results}
            
            # 为每个推荐添加详细信息
            for i in range(len(recommendations)):
                course_id, score = recommendations[i]
                if course_id in course_info:
                    info = course_info[course_id]
                    recommendations[i] = (course_id, score, info)
            
        except Exception as e:
            print(f"添加推荐详细信息失败: {e}")
    
    def save_recommendation_to_redis(self, user_id, recommendations, ttl=3600):
        """将推荐结果保存到Redis，设置过期时间"""
        try:
            if not self.redis_conn:
                self.redis_conn = get_redis_conn()
            
            # 将推荐结果转换为JSON格式
            recommendations_json = json.dumps([
                {'course_id': course_id, 'score': score} 
                for course_id, score in recommendations
            ])
            
            # 保存到Redis
            key = f"recommendation:user:{user_id}"
            self.redis_conn.set(key, recommendations_json, ex=ttl)
            
            print(f"已将用户 {user_id} 的推荐结果保存到Redis，过期时间 {ttl} 秒")
            return True
            
        except Exception as e:
            print(f"保存推荐结果到Redis失败: {e}")
            return False
    
    def get_recommendation_from_redis(self, user_id):
        """从Redis获取用户的推荐结果"""
        try:
            if not self.redis_conn:
                self.redis_conn = get_redis_conn()
            
            # 从Redis获取推荐结果
            key = f"recommendation:user:{user_id}"
            recommendations_json = self.redis_conn.get(key)
            
            if not recommendations_json:
                return None
            
            # 解析JSON
            recommendations = json.loads(recommendations_json)
            
            # 转换为标准格式
            return [(item['course_id'], item['score']) for item in recommendations]
            
        except Exception as e:
            print(f"从Redis获取推荐结果失败: {e}")
            return None

# 测试代码
if __name__ == "__main__":
    # 创建混合推荐器实例
    hybrid_recommender = HybridRecommender()
    
    # 如果模型未训练，进行训练
    if not (hasattr(hybrid_recommender.item_cf, 'item_similarity_matrix') and 
            hasattr(hybrid_recommender.content_recommender, 'content_similarity')):
        print("模型未训练，开始训练...")
        hybrid_recommender.train_models()
    
    # 测试推荐功能
    user_id = 1  # 替换为实际用户ID
    recommendations = hybrid_recommender.recommend(user_id, n=5)
    
    print(f"\n为用户 {user_id} 的混合推荐课程:")
    for rec in recommendations:
        if len(rec) == 2:
            course_id, score = rec
            print(f"课程ID: {course_id}, 混合分数: {score:.2f}")
        else:
            course_id, score, info = rec
            print(f"课程ID: {course_id}, 混合分数: {score:.2f}, 标题: {info['title']}, 类别: {info['category']}") 